﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using rentit03_server.Database;

namespace rentit03_server.Business
{
    /// <summary>
    /// Class responsible for sessions
    /// </summary>
    static public class SessionHandler
    {
        private static readonly Dictionary<string, Session> SessionIds = new Dictionary<string, Session>();
        private static readonly DataAccessObject Dao = new DataAccessObject();

        /// <summary>
        /// Log in a user. Given a valid username and password will return a session id.
        /// </summary>
        /// <param name="username">Username - the e-mail address for the user</param>
        /// <param name="password">Password</param>
        /// <returns>The session id that should later be used when making request on behalf of that user</returns>
        /// <exception cref="SessionHandlerException">Throws exception if username and password doesn't match</exception>
        public static string Login(string username, string password)
        {
            // Get user with login information
            var user = Dao.AuthenticateUser(username, EncryptPassword(password));

            if (user == null)
            {
                // If session id wasn't returned throw exception
                throw new SessionHandlerException(ReturnStatus.SessionInvalidLoginInformation, "The username and password did not match");
            }

            // Get unique session id
            var time = DateTime.Now;
            var sessionInt = (user.Email + user.Id + time).GetHashCode();
            var sessionId = sessionInt.ToString();
            while (SessionIds.ContainsKey(sessionId))
            {
                sessionId = (++sessionInt).ToString();
            }

            // Create session
            SessionIds.Add(sessionId, new Session(user, time));

            return sessionId;
        }

        /// <summary>
        /// Log out a user.
        /// </summary>
        /// <param name="sessionId">The user's current session id</param>
        /// <exception cref="SessionHandlerException">Throws exception if session id is invalid. May happen if the session id has expired</exception>
        public static void Logout(string sessionId)
        {
            if (IsValidSession(sessionId))
                SessionIds.Remove(sessionId);
        }

        /// <summary>
        /// Check if a session id is valid
        /// </summary>
        /// <param name="sessionId">Session id</param>
        /// <returns>True if session id is valid, otherwise false</returns>
        private static bool IsValidSession(string sessionId)
        {
            if (SessionIds.ContainsKey(sessionId))
            {
                var session = SessionIds[sessionId];

                var limit = DateTime.Now.Subtract(new TimeSpan(0, 30, 0));
                if (session.Time < limit)
                {
                    SessionIds.Remove(sessionId);
                    throw new SessionHandlerException(ReturnStatus.SessoinExpiredSessionId, "Session id has expired");
                }

                session.Time = DateTime.Now;

                return true;
            }
            throw new SessionHandlerException(ReturnStatus.SessionInvalidSessionId, "Session id is invalid");
        }

        /// <summary>
        /// Check if user has PermissionEnum
        /// </summary>
        /// <param name="permissionEnum">Name of the PermissionEnum</param>
        /// <param name="sessionId">Session id</param>
        /// <param name="throwException">If true an exception will be thrown if the user doesn't have access </param>
        /// <param name="userId">If the user id matches the session's user's id access will always be true</param>
        /// <returns>True if user has the PermissionEnum, false otherwise</returns>
        public static bool UserAccess(PermissionEnum permissionEnum, string sessionId, bool throwException = false, int userId = -1)
        {
            var user = GetUser(sessionId);

            // Admin (user 1) has access to everything!
            var access = user.Id == 1 || GetUserPermissions(user).Any(p => p.Id == (int)permissionEnum) || userId == user.Id;

            if (throwException && !access)
                throw new SessionHandlerException(ReturnStatus.AccessPermissionDenied, "Access denied! Access level needed: " + permissionEnum);

            return access;
        }

        /// <summary>
        /// Get a list of a given users permissions
        /// </summary>
        /// <param name="user">The user of which you want the roles</param>
        /// <returns>A list of permissions</returns>
        public static List<Permission> GetUserPermissions(User user)
        {
            return user.Roles.Select(role => Dao.ReadRole(role.Name)).SelectMany(r => r.Permissions).ToList();
        }

        /// <summary>
        /// Get the user object associated with a given session
        /// </summary>
        /// <param name="sessionId">Session id</param>
        /// <returns>User</returns>
        /// <exception cref="SessionHandlerException">Throws exception if session id is invalid. May happen if the session id has expired</exception>
        public static User GetUser(string sessionId)
        {
            if (String.IsNullOrWhiteSpace(sessionId))
            {
                var user = new User { Id = 0, Name = "Anonymous User" };

                var role = Dao.ReadRole("Anonymous User");
                if (role != null)
                    user.Roles.Add(role);

                return user;
            }

            IsValidSession(sessionId);
            return SessionIds[sessionId].User;
        }

        /// <summary>
        /// Encrypt a password
        /// </summary>
        /// <param name="password">Unencrypted password</param>
        /// <returns>Encrypted password</returns>
        public static string EncryptPassword(string password)
        {
            var computeHash = new SHA1CryptoServiceProvider().ComputeHash(Encoding.UTF8.GetBytes(password));
            var stringBuilder = new StringBuilder();
            foreach (var b in computeHash)
            {
                stringBuilder.Append(b.ToString("x2").ToLower());
            }
            return stringBuilder.ToString();
        }

        /// <summary>
        /// Representation of a session.
        /// </summary>
        private struct Session
        {
            public Session(User user, DateTime time)
                : this()
            {
                User = user;
                Time = time;
            }

            public User User { get; private set; }
            public DateTime Time { get; set; }
        }

        /// <summary>
        /// Create a new role
        /// </summary>
        /// <param name="role">Name of the role</param>
        /// <param name="sId">Session id</param>
        /// <returns>Id of the newly created role</returns>
        public static int CreateRole(RentitRole role, string sId)
        {
            UserAccess(PermissionEnum.CreateUserRole, sId, true);
            try
            {
                return Dao.CreateRole(role);
            }
            catch (Exception e)
            {
                throw new RentitException("Create role error!", e);
            }
        }

        /// <summary>
        /// Read a role
        /// </summary>
        /// <param name="rId">Id of the role you want to read</param>
        /// <param name="sId">Session id</param>
        /// <returns>A RentitRole</returns>
        public static RentitRole ReadRole(int rId, string sId)
        {
            UserAccess(PermissionEnum.ReadUserRole, sId, true);

            var role = Dao.ReadRole(rId);

            if(role == null)
                throw new SessionHandlerException(ReturnStatus.RoleNotExists, "The role ID didn't match any role");
            
            return Dao.ReadRole(rId);
        }

        /// <summary>
        /// Create a new permission
        /// </summary>
        /// <param name="permission">Name of the new permission</param>
        /// <param name="sId">Session id</param>
        /// <returns>Id of the newly created permission</returns>
        public static int CreatePermission(RentitPermission permission, string sId)
        {
            UserAccess(PermissionEnum.CreateUserPermission, sId, true);
            try
            {
                return Dao.CreatePermission(permission);
            }
            catch (Exception e)
            {
                throw new RentitException("Create permission error!", e);
            }
        }
    }

    /// <summary>
    /// Custom exception used by the SessionHandler
    /// </summary>
    public class SessionHandlerException : RentitException
    {
        public SessionHandlerException(ReturnStatus status, string message) : base(status, message) { }
    }

    /// <summary>
    /// Representation of permissions as integers
    /// </summary>
    public enum PermissionEnum
    {
        Session = 100,

        User = 200,
        CreateUser,
        ReadUser,
        ReadBasicUserInfo,
        ReadAllUserInfo,
        EditUser,
        DeleteUser,
        OverviewUser,
        ReadUserCredits,
        EditUserCredits,

        Media = 300,
        CreateMedia,
        ReadMedia,
        EditMedia,
        DeleteMedia,
        OverviewMedia,
        OverviewMediaVersions,
        EditVersion,

        Comment = 350,
        CreateComment,
        ReadComment,
        EditComment,
        DeleteComment,

        Permissions = 400,
        ReadUserPermissions,
        ReadUserRole,
        CreateUserRole,
        DeleteUserRole,
        CreateUserPermission,
        EditUserPermission,
        DeleteUserPermission,
        OverviewUserPermission,
    }
}